use std::{
    io::{self, Read},
    sync::mpsc::{self, Receiver, SyncSender, TrySendError},
    thread, time,
};

use crate::{
    device::Device,
    gpu::{SCREEN_HEIGHT, SCREEN_WIDTH},
    KeypadKey,
};
use crate::{gbevent::GBEvent, player::audioplayer::NullAudioPlayer};
use std::sync::mpsc::TryRecvError;

#[derive(Default)]
pub struct RenderOptions {
    pub linear_interpolation: bool,
}

// TODO: migrate WindowBuilder to WindowAttributes
// bash: cargo doc --open

// #[cfg(target_os = "windows")]
// pub(crate) fn create_window_builder(rom_file: &str) -> winit::window::WindowAttributes {
//     use winit::window::WindowAttributes;

//     let mut attr = WindowAttributes::default();

//     attr.with_resizable(false);
//     attr.with_title("GayBoy = Oh yes boy running: ".to_owned() + rom_file);
//     attr
// }

#[cfg(target_os = "linux")]
pub fn create_window_builder(rom_file: &str) -> winit::window::WindowAttributes {
    use winit::window::WindowAttributes;

    WindowAttributes::default().with_title("GayBoy = Oh yes boy running: ".to_owned() + rom_file)
}

#[allow(private_interfaces)]
pub fn recalculate_screen<
    T: glium::glutin::surface::SurfaceTypeTrait + glium::glutin::surface::ResizeableSurface + 'static,
>(
    display: &glium::Display<T>,
    texture: &mut glium::texture::texture2d::Texture2d,
    datavec: &[u8],
    render_options: &RenderOptions,
) {
    use glium::Surface;

    let interpolation_type = if render_options.linear_interpolation {
        glium::uniforms::MagnifySamplerFilter::Linear
    } else {
        glium::uniforms::MagnifySamplerFilter::Nearest
    };

    let rawimage2d = glium::texture::RawImage2d {
        data: std::borrow::Cow::Borrowed(datavec),
        width: SCREEN_WIDTH as u32,
        height: SCREEN_HEIGHT as u32,
        format: glium::texture::ClientFormat::U8U8U8,
    };

    texture.write(
        glium::Rect {
            left: 0,
            bottom: 0,
            width: SCREEN_WIDTH as u32,
            height: SCREEN_HEIGHT as u32,
        },
        rawimage2d,
    );

    // We use a custom BlitTarget to transform OpenGL coordinates to row-column
    // coordinates
    let target = display.draw();
    let (target_w, target_h) = target.get_dimensions();

    texture.as_surface().blit_whole_color_to(
        &target,
        &glium::BlitTarget {
            left: 0,
            bottom: target_h,
            width: target_w as i32,
            height: -(target_h as i32),
        },
        interpolation_type,
    );

    target.finish().unwrap();
}

pub fn construct_cpu(
    filename: &str,
    classic_mode: bool,
    output_serial: bool,
    output_printer: bool,
    skip_checksum: bool,
) -> Option<Box<Device>> {
    let opt = match classic_mode {
        true => Device::new(filename, skip_checksum),
        false => Device::new_cgb(filename, skip_checksum),
    };

    let mut o = match opt {
        Ok(cpu) => cpu,
        Err(msg) => {
            eprintln!("{}", msg);
            return None;
        }
    };

    if output_printer {
        o.attach_printer();
    } else {
        o.set_stdout(output_serial);
    }
    Some(Box::new(o))
}

pub fn run_test_mode(filename: &str, classic_mode: bool, skip_checksum: bool) -> i32 {
    let opt_cpu = match classic_mode {
        true => Device::new(filename, skip_checksum),
        false => Device::new_cgb(filename, skip_checksum),
    };
    let mut cpu = match opt_cpu {
        Err(errmsg) => {
            eprintln!("{}", errmsg);
            return -1;
        }
        Ok(cpu) => cpu,
    };

    cpu.set_stdout(true);
    cpu.enable_audio(Box::new(NullAudioPlayer {}));

    // from masonforest, https://stackoverflow.com/a/55201400 (CC BY-SA 4.0)
    let stdin_channel = spawn_stdin_channel();
    loop {
        match stdin_channel.try_recv() {
            Ok(stdin_byte) => match stdin_byte {
                b'q' => break,
                b's' => {
                    let data = cpu.get_gpu_data().to_vec();
                    print_screenshot(data);
                }
                v => {
                    eprintln!("MSG:Unknown stdinvalue {}", v);
                }
            },
            Err(TryRecvError::Empty) => {}
            Err(TryRecvError::Disconnected) => break,
        }
        for _ in 0..1000 {
            cpu.do_cycle();
        }
    }
    0
}

fn spawn_stdin_channel() -> Receiver<u8> {
    let (tx, rx) = mpsc::channel::<u8>();
    thread::spawn(move || loop {
        let mut buf = [0];
        match io::stdin().read(&mut buf) {
            Ok(1) => tx.send(buf[0]).unwrap(),
            Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
            _ => break,
        }
    });

    rx
}

fn print_screenshot(data: Vec<u8>) {
    eprint!("SCREENSHOT: ");
    for b in data {
        eprint!("{:02x}", b);
    }
    eprintln!();
}

pub fn set_window_size(window: &winit::window::Window, scale: u32) {
    let _ = window.request_inner_size(winit::dpi::LogicalSize::<u32>::from((
        SCREEN_WIDTH as u32 * scale,
        SCREEN_HEIGHT as u32 * scale,
    )));
}

fn timer_periodic(ms: u64) -> Receiver<()> {
    let (tx, rx) = mpsc::sync_channel(1);
    thread::spawn(move || loop {
        thread::sleep(time::Duration::from_millis(ms));

        if tx.send(()).is_err() {
            break;
        }
    });
    rx
}

pub fn winit_to_keypad(keycode: winit::keyboard::Key<&str>) -> Option<KeypadKey> {
    use winit::keyboard::{Key, NamedKey};
    match keycode {
        Key::Character("Z" | "z") => Some(KeypadKey::A),
        Key::Character("X" | "x") => Some(KeypadKey::B),
        Key::Named(NamedKey::ArrowUp) => Some(KeypadKey::Up),
        Key::Named(NamedKey::ArrowDown) => Some(KeypadKey::Down),
        Key::Named(NamedKey::ArrowLeft) => Some(KeypadKey::Left),
        Key::Named(NamedKey::ArrowRight) => Some(KeypadKey::Right),
        Key::Named(NamedKey::Space) => Some(KeypadKey::Select),
        Key::Named(NamedKey::Enter) => Some(KeypadKey::Start),
        _ => {
            eprintln!("supported keycodes: [z, x, space, enter]");
            None
        }
    }
}

pub fn run_cpu(mut cpu: Box<Device>, sender: SyncSender<Vec<u8>>, receiver: Receiver<GBEvent>) {
    let periodic = timer_periodic(16);
    let mut limit_speed = true;

    let waitticks = (4194304f64 / 1000.0 * 16.0).round() as u32;
    let mut ticks = 0;

    'outer: loop {
        while ticks < waitticks {
            ticks += cpu.do_cycle();
            if cpu.check_and_reset_gpu_updated() {
                let data = cpu.get_gpu_data().to_vec();
                if let Err(TrySendError::Disconnected(..)) = sender.try_send(data) {
                    break 'outer;
                }
            }
        }

        ticks -= waitticks;

        'recv: loop {
            match receiver.try_recv() {
                Ok(event) => match event {
                    GBEvent::KeyUp(key) => cpu.keyup(key),
                    GBEvent::KeyDown(key) => cpu.keydown(key),
                    GBEvent::SpeedUp => limit_speed = false,
                    GBEvent::SpeedDown => {
                        limit_speed = true;
                        cpu.sync_audio();
                    }
                },
                Err(TryRecvError::Empty) => break 'recv,
                Err(TryRecvError::Disconnected) => break 'outer,
            }
        }

        if limit_speed {
            let _ = periodic.recv();
        }
    }
}
